package com.anasoft.os.s4j;

import org.junit.runner.Runner;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.Suite;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;

import com.anasoft.os.s4j.annotation.ErrorReporting;
import com.anasoft.os.s4j.annotation.SeleniumSession;
import com.anasoft.os.s4j.util.AnnotationFinder;
import com.thoughtworks.selenium.Selenium;

/**
 * JUnit {@link Runner} for running test suites within a {@link Selenium}
 * session.
 * <p>
 * Typically, you might want to run your Selenium tests in a predictable
 * order. This is achieved by placing the <tt>SuiteClasses</tt> annotation
 * over your test suite classes.
 * 
 * <pre>
 * &#064;RunWith(SeleniumSuiteRunner.class)
 * &#064;SuiteClasses({
 *      MySeleniumTestOne.class,
 *      MySeleniumTestTwo.class
 * })
 * &#064;SeleniumSession(
 *      browserString = "*firefox",
 *      browserUrl = "http://localhost:8080"
 * )
 * public class MySeleniumTestSuite {
 *      ...
 * }
 * </pre>
 * 
 * This runner is configured by placing a {@link SeleniumSession} annotation
 * over your test classes (either directly or via custom annotations). See
 * the {@link SeleniumTestRunner} documentation for more information about
 * using this annotation to configure your Selenium tests.
 * <p>
 * In addition to running your Selenium tests in a predictable sequence, you
 * might also want to specify some global setup/teardown logic which gets
 * called before/after all your tests. This is particularly useful if you
 * have more than one test suite.
 * 
 * <pre>
 * // annotations omitted for brevity
 * public class MySeleniumTestSuite {
 *      &#064;BeforeClass
 *      public static void cleanupDatabaseAndInitApplication() {
 *          Selenium selenium = SeleniumSuiteRunner.getSelenium();
 *          ...
 *      }
 * }
 * </pre>
 * 
 * As shown above, test suite classes can use the {@link #getSelenium()
 * getSelenium} method to retrieve the current {@link Selenium} session
 * (the session in which all of their test methods are executed). This
 * session is automatically started by the runner before running your
 * test suites and stopped after all test suites have finished.
 * <p>
 * There is exactly one {@link Selenium} session opened for each runner
 * instance. The session is held as a thread-local variable inside the
 * runner, so you can freely run your Selenium tests in a multi-threaded
 * way.
 * <p>
 * All Selenium based runners share some common operations by implementing
 * the {@link SeleniumRunnerLogic} interface. The actual implementation
 * of this interface comes from the {@link #getRunnerLogic() getRunnerLogic}
 * method and can be overriden in case you want to customize the default
 * runner behavior.
 * 
 * @see SeleniumSession
 * @see SeleniumTestRunner
 * @see SeleniumRunnerLogic
 * 
 * @author vojtech.szocs
 */
public class SeleniumSuiteRunner extends Suite implements SeleniumRunnerLogic {

    private static final ThreadLocal<Selenium> sessionHolder = new ThreadLocal<Selenium>();
    
    private final SeleniumSession sessionConfig;
    private final SeleniumRunnerLogic runnerLogic;
    
    private boolean skipSubsequentTestsOnFailure = false;
    
    public SeleniumSuiteRunner(Class<?> clazz, RunnerBuilder builder) throws InitializationError {
        super(clazz, builder);
        
        sessionConfig = AnnotationFinder.find(clazz, SeleniumSession.class);
        
        if (sessionConfig == null)
            throw new InitializationError(SeleniumSession.class.getSimpleName()
                    + " annotation must be placed over your test class for this runner: "
                    + clazz.getSimpleName());
        
        runnerLogic = getRunnerLogic();
    }
    
    /**
     * Override this method in case you want to customize the default
     * runner behavior.
     * 
     * @return {@link SeleniumRunnerLogic} implementation to be used.
     */
    protected SeleniumRunnerLogic getRunnerLogic() {
        return DefaultSeleniumRunnerLogic.INSTANCE;
    }
    
    /**
     * @see org.junit.runners.ParentRunner#run(org.junit.runner.notification.RunNotifier)
     */
    @Override
    public void run(final RunNotifier notifier) {
        SeleniumSessionManager.doInSession(sessionConfig, sessionHolder, new Runnable() {
            
            public void run() {
                notifier.addListener(getErrorReportingListener(
                        sessionConfig.errorReporting(), getSelenium()));
                
                if (sessionConfig.skipSubsequentTestsOnFailure()) {
                    notifier.addListener(new RunListener() {
                        
                        @Override
                        public void testFailure(Failure failure) throws Exception {
                            skipSubsequentTestsOnFailure = true;
                        }
                        
                    });
                }
                
                SeleniumSuiteRunner.super.run(notifier);
            }
            
        }, runnerLogic.getSessionListener());
    }
    
    /**
     * @see org.junit.runners.Suite#runChild(org.junit.runner.Runner, org.junit.runner.notification.RunNotifier)
     */
    @Override
    protected void runChild(Runner runner, RunNotifier notifier) {
        if (!skipSubsequentTestsOnFailure)
            super.runChild(runner, notifier);
    }
    
    /**
     * Returns the reference to the current {@link Selenium} session.
     * <p>
     * Test classes can safely call this method in non-static blocks
     * since the session is started before the actual test class gets
     * instantiated by the runner.
     * 
     * @return Selenium API for executing commands on the current
     * session.
     */
    public static Selenium getSelenium() {
        return sessionHolder.get();
    }
    
    /**
     * @see com.anasoft.os.s4j.SeleniumRunnerLogic#getErrorReportingListener(com.anasoft.os.s4j.annotation.ErrorReporting, com.thoughtworks.selenium.Selenium)
     */
    public final RunListener getErrorReportingListener(ErrorReporting errorConfig, Selenium session) {
        return runnerLogic.getErrorReportingListener(errorConfig, session);
    }
    
    /**
     * @see com.anasoft.os.s4j.SeleniumRunnerLogic#getSessionListener()
     */
    public final SeleniumSessionListener getSessionListener() {
        return runnerLogic.getSessionListener();
    }
    
}
